#!/usr/bin/env ruby
# coding: utf-8

require 'find'

# These members/tags are common to multiple events
BYTE_SIZE_COUNT = ['byte_size', 'count']

def hash_array_add(hash, key, item)
  arr = hash.fetch(key, Array::new)
  arr.append(item)
  hash[key] = arr
end

# A class to hold error reports and common functionality
class Event
  attr_accessor :path
  attr_reader :name, :reports
  attr_writer :members

  def initialize(name)
    @path = nil
    @name = name
    @reports = []
    @members = []
    @counters = {}
    @logs = []
  end

  def add_counter(name, tags)
    @counters[name] = tags
  end

  def add_log(type, message, parameters)
    @logs.append([type, message, parameters])
  end

  def valid?
    @reports.clear

    # Check BytesReceived events (for sources)
    if @name.end_with? 'BytesReceived'
      members_must_include(['byte_size'])
      counters_must_include('component_received_bytes_total', ['protocol'] + @members - ['byte_size'])
    end

    # Check EventsReceived events (common)
    if @name.end_with? 'EventsReceived'
      members_must_include(BYTE_SIZE_COUNT)
      counters_must_include('component_received_events_total', @members - BYTE_SIZE_COUNT)
      counters_must_include('component_received_event_bytes_total', @members - BYTE_SIZE_COUNT)
    end

    # Check EventsSent events (common)
    if @name.end_with? 'EventsSent'
      members_must_include(BYTE_SIZE_COUNT)
      counters_must_include('component_sent_events_total', @members - BYTE_SIZE_COUNT)
      counters_must_include('component_sent_event_bytes_total', @members - BYTE_SIZE_COUNT)
    end

    # Check BytesSent events (for sinks)
    if @name.end_with? 'BytesSent'
      members_must_include(['byte_size'])
      counters_must_include('component_sent_bytes_total', ['protocol'] + @members - ['byte_size'])
    end

    has_errors = @logs.one? { |type, _, _| type == 'error' }

    # Make sure Error events output an error
    if has_errors or @name.end_with? 'Error'
      append('Error events MUST be named "___Error".') unless @name.end_with? 'Error'
      counters_must_include('component_errors_total', ['error_type', 'stage'] + @members - ['body', 'error', 'text'])
    end

    # Make sure error events contain the right parameters
    @logs.each do |type, message, parameters|
      if type == 'error'
        ['error', 'stage'].each do |parameter|
          unless parameters.include? parameter
            @reports.append("Error log MUST include parameter \"#{parameter}\".")
          end
        end
      end
    end

    @reports.empty?
  end

  private

    def append(report)
      @reports.append(report)
    end

    def generic_must_contain(array, names, prefix, suffix)
      names.each do |name|
        unless array.include? name
          @reports.append("#{prefix} MUST #{suffix} \"#{name}\".")
        end
      end
    end

    def counters_must_include(name, required_tags)
      unless @counters.include? name
        @reports.append("This event MUST increment counter \"#{name}\".")
      else
        tags = @counters[name]
        required_tags.each do |tag|
          unless tags.include? tag
            @reports.append("Counter \"#{name}\" MUST include tag \"#{tag}\".")
          end
        end
      end
    end

    def members_must_include(names)
      generic_must_contain(@members, names, 'This event', 'have a member named')
    end
end

$all_events = Hash::new { |hash, key| hash[key] = Event::new(key) }

error_count = 0

# Scan sources and build internal structures
Find.find('.') do |path|
  if path.end_with? '.rs'
    text = File.read(path)

    # Check log message texts for correct formatting. See below for the
    # full regex
    if (path.start_with? 'src/')
      text.scan(/(trace|debug|info|warn|error)!\(\s*(message\s*=\s*)?"([^({)][^("]+)"/) do
        |type, has_message_prefix, message|
        reports = []
        reports.append('Message must start with a capital.') unless message.match(/^[[:upper:]]/)
        reports.append('Message must end with a period.') unless message.match(/\.$/)
        unless reports.empty?
          puts "#{path}: Errors in message \"#{message}\":"
          reports.each { |report| puts "  #{report}" }
          error_count += 1
        end
      end
    end

    if (path.start_with? 'src/internal_events/' or path.start_with? 'lib/vector_core/core_common/src/internal_event/') and !text.match?(/## skip check-events ##/i)
      # Scan internal event structs for member names
      text.scan(/[\n ]struct (\S+?)(?:<.+?>)?(?: {\n(.+?)\n\s*}|;)\n/m) do |struct_name, members|
        $all_events[struct_name].path = path
        if members
          member_names = members.scan(/ ([A-Za-z0-9_]+): /).map { |member,| member }
          $all_events[struct_name].members = member_names
        end
      end

      # Scan internal event implementation blocks for logs and metrics
      text.scan(/^(\s*)impl(?:<.+?>)? InternalEvent for ([A-Za-z0-9_]+)(?:<.+?>)? {\n(.+?)\n\1}$/m) do |_space, event_name, block|
        # Scan for counter names and tags
        block.scan(/ counter!\((?:\n\s+)?"([^"]+)",(.+?)\)[;\n]/m) do |name, tags|
          tags = tags.scan(/"([^"]+)" => /).map { |tag,| tag }
          $all_events[event_name].add_counter(name, tags)
        end

        # Scan for log outputs and their parameters
        block.scan(/
                    (trace|debug|info|warn|error)! # The log type
                    \(\s*(?:message\s*=\s*)? # Skip any leading "message =" bit
                    "([^({)][^("]+)" # The log message text
                    ([^;]*?) # Match the parameter list
                    \)(?:;|\n\s*}) # Normally would end with simply ");", but some are missing the semicolon
                   /mx) do |type, message, parameters|
          parameters = parameters.scan(/([a-z0-9_]+) *= .|[?%]([a-z0-9_.]+)/) \
                         .map { |assignment, simple| assignment or simple }

          $all_events[event_name].add_log(type, message, parameters)
        end
      end
    end
  end
end

$all_events.each_value do |event|
  unless event.valid?
    puts "#{event.path}: Errors in event #{event.name}:"
    event.reports.each { |report| puts "    #{report}" }
    error_count += 1
  end
end

puts "#{error_count} error(s)"
exit 1 if error_count > 0
